<?php

abstract class Jf_Orm_Resource_Abstract extends Jf_Db_Table_Abstract {

    protected $_cacheDependentsKey;

    protected $_filters = array();

    protected $_counter = array();

    protected $_orders = array();

    protected $_indexCols = array();

    protected $_relationMap = array();

    protected $_activeRelations = array();

    protected $_joinRelation = false;

    protected $_enbledM2m = true;

    protected $_index = array();

    protected $_saveData = array();

    protected $_saveId = null;

    protected $_isUpdate = false;

    protected $_row = null;

    protected $_rows = null;

    protected $_autoCount = array();

    protected $_bind = array();

    protected $_sql = array();

    protected $_joinTable = array();

    public function save(array $data, $id = null) {
        $this->_saveData = $data;
        $this->_saveId = $id;
        $this->_db->beginTransaction();
        try {
            if (is_numeric($this->_saveId)) {
                $this->_isUpdate = true;
            } else {
                $this->_isUpdate = false;
            }
            $this->_beforeSave();
            if ($this->_isUpdate) {
                $this->updateByPrimary($this->_saveData, $this->_saveId);
            } else {
                $result = $this->insert($this->_saveData);
                if (! $this->_saveId) {
                    $this->_saveId = $result;
                }
            }
            $this->_afterSave();
            $this->_db->commit();
        } catch (Exception $e) {
            $this->_db->rollBack();
            throw new Exception($e->getMessage());
            return false;
        }
        return $this->_saveId;
    }

    protected function insertSuccess() {
        return ! $this->_isUpdate && $this->_saveId;
    }

    protected function _beforeSave() {}

    protected function _afterSave() {}

    protected function _beforeDelete() {}

    public function addCacheDependent($dependent) {
        $dependent = str_replace('Model_Resource', 'Model', $dependent);
        $cacheKey = $this->getCacheDependentsKey();
        $cacheDependents = Jf::cache()->get($cacheKey);
        if (false === $cacheDependents) {
            $cacheDependents = array();
        }
        if (! in_array($dependent, $cacheDependents)) {
            array_push($cacheDependents, $dependent);
            Jf::cache()->set($cacheKey, $cacheDependents);
        }
        return $this;
    }

    public function setCacheDependents(array $dependents) {
        $cacheKey = $this->getCacheDependentsKey();
        Jf::cache()->set($cacheKey, $dependents);
        return $this;
    }

    public function getCacheDependents() {
        $cacheKey = $this->getCacheDependentsKey();
        Jf::cache()->get($cacheKey);
        return Jf::cache()->get($cacheKey);
    }

    public function getCacheDependentsKey() {
        if (! $this->_cacheDependentsKey) {
            $this->_cacheDependentsKey = get_class($this) . '/cacheDependents';
        }
        return $this->_cacheDependentsKey;
    }

    protected function _processRelation() {
        foreach ($this->_activeRelations as $alis) {
            $relation = $this->_relationMap[$alis];
            $fkey = $relation['fkey'];
            $mkey = $relation['mkey'];
            if (empty($this->_row[$mkey])) {
                unset($this->_row[$alis]);
                continue;
            }
            $value = $this->_row[$mkey];
            $o2m = isset($relation['o2m']) ? $relation['o2m'] : false;
            $o2o = isset($relation['o2o']) ? $relation['o2o'] : false;
            $sort_order = isset($relation['sort_order']) ? $relation['sort_order'] : null;
            $group = isset($relation['group']) ? $relation['group'] : null;
            $enableCache = isset($relation['enableCache']) ? $relation['enableCache'] : false;
            $relationModel = Jf::getModel($relation['modelName']);
            if (true === $enableCache) {
                $relationModel->enableCache()->addCacheDependent(get_class($this));
            }
            $columns = (array) $relation['columns'];
            if (true === $o2m) {
                $count = isset($relation['count']) ? $relation['count'] : null;
                $rows = $relationModel->fetchAll($columns, array(
                    $fkey => $value
                ),  $sort_order, $count, null, $group);
                $this->_row[$alis] = $rows;
            } elseif (true === $o2o) {
                $o2oMerge = isset($relation['o2o_merge']) ? $relation['o2o_merge'] : true;
                array_unshift($columns, $fkey);
                $mRow = $relationModel->fetchRow($columns, array(
                    $fkey => $value
                ), $sort_order, $group);
                if (empty($mRow)) {
                    unset($this->_row[$alis]);
                    continue;
                }
                if (! $this->_relationHasChildren($columns) && ! in_array($fkey, (array) $relation['columns'])) {
                    unset($mRow[$fkey]);
                    if ($fkey !== $this->_primary) {
                        // unset($this->_row[$mkey]);
                    }
                }
                if ($o2oMerge && ! empty($mRow)) {
                    $this->_row = array_merge($this->_row, $mRow);
                } elseif (! empty($mRow)) {
                    $this->_row[$alis] = $mRow;
                }
            }
        }
        return $this;
    }

    protected function _processRelationsByJoin() {
        foreach ($this->_activeRelations as $alias) {
            $relation = $this->_relationMap[$alias];
            if (empty($relation['tableName'])) {
                continue;
            }
            $tableName = $relation['tableName'];
            if (! isset($relation['columns'])) {
                $columns = '*';
            } elseif (is_array($relation['columns'])) {
                $columns = array_intersect($relation['columns'], $this->_getDbTableCols($tableName));
            } else {
                $columns = null;
            }
            $joinType = empty($relation['is_required']) ? 'joinLeft' : 'joinInner';
            $this->getSelect()->{$joinType}(array(
                $tableName => $this->getDbTable($tableName)
            ), "$this->_name.{$relation['mkey']} = $tableName.{$relation['fkey']}", $columns);
            if (! empty($relation['where'])) {
                $this->_where($relation['where']);
            }
        }
        return $this;
    }

    public function setSql($sql) {
        $this->_sql[] = $sql;
        return $this;
    }

    public function getSql() {
        if (count($this->_sql) === 1) {
            return array_shift($this->_sql);
        } else {
            return $this->_sql;
        }
    }

    protected function hasJoinTable($tableName) {
        return in_array($tableName, $this->_joinTable);
    }

    protected function joinTable($tableName) {
        array_push($this->_joinTable, $tableName);
        return $this;
    }

    public function reset() {
        $this->_joinRelation = false;
        $this->_joinTable = array();
        $this->_activeRelations = array();
        $this->_select->reset();
        $this->resetBind();
    }

    public function fetchAll($columns = '*', $filters = null, $orders = null, $count = null, $offset = null, $group = null) {
        $this->reset();
        $this->_select->from(array(
            $this->_name => $this->_name
        ));
        $this->setColumns($columns);
        if ($this->_joinRelation && ! empty($this->_relationMap)) {
            $this->_processRelationsByJoin();
        }
        if ($filters !== null) {
            $this->setFilters($filters);
        }
        if ($orders !== null) {
            $this->setOrders($orders);
        }
        if ($count !== null || $offset !== null) {
            $this->_select->limit($count, $offset);
        }
        if ($group !== null) {
            $this->_select->group($group);
        }
        $sql = $this->_select->assemble();
        $this->setSql($sql);
        $this->_rows = $this->_db->fetchAll($sql, $this->_bind);
        if ($this->_rows && isset($this->_indexCols)) {
            if (version_compare(PHP_VERSION, '5.5.0') >= 0) {
                $this->setIndexByRows();
            } else {
                $this->setIndex();
            }
            if ($this->_enbledM2m) {
                $this->getManyToMany();
            }
        }
        return $this->_rows;
    }

    public function fetchCol($columns, $filters = array(), $orders = null, $count = null, $offset = null, $group = null) {
        $this->reset();
        $this->_select->from(array(
            $this->_name => $this->_name
        ));
        $this->setColumns($columns);
        if ($this->_joinRelation && ! empty($this->_relationMap)) {
            $this->_processRelationsByJoin();
        }
        if ($filters !== null) {
            $this->setFilters($filters);
        }
        if ($orders !== null) {
            $this->setOrders($orders);
        }
        if ($count !== null || $offset !== null) {
            $this->_select->limit($count, $offset);
        }
        if ($group !== null) {
            $this->_select->group($group);
        }
        $sql = $this->_select->assemble();
        $this->setSql($sql);
        return $this->_db->fetchCol($sql, $this->_bind);
    }

    public function fetchPairs($columns, $filters) {
        $this->reset();
        $this->_select->from($this->_name, $this->getIntersectCols($columns));
        if ($filters !== null) {
            $this->setFilters($filters);
        }
        $sql = $this->_select->assemble();
        return $this->_db->fetchPairs($sql, $this->_bind);
    }

    public function fetchRow($columns = '*', $filters = null, $orders = null, $group = null) {
        $this->reset();
        $this->_select->from(array(
            $this->_name => $this->_name
        ));
        $this->setColumns($columns);
        if ($this->_joinRelation && ! empty($this->_relationMap)) {
            $this->_processRelationsByJoin();
            ;
        }
        if ($filters !== null) {
            $this->setFilters($filters);
        }
        if ($orders !== null) {
            $this->setOrders($orders);
        }
        if ($group !== null) {
            $this->_select->group($group);
        }
        $sql = $this->_select->assemble();
        $this->setSql($sql);
        $this->_row = $this->_db->fetchRow($sql, $this->_bind);
        if ($this->_activeRelations && $this->_row) {
            $this->_processRelation();
        }
        return $this->_row;
    }

    public function fetchOne($columns, $filters, $orders = null, $group = null) {
        $this->reset();
        $this->_select->from($this->_name, $columns);
        if ($filters !== null) {
            $this->setFilters($filters);
        }
        if ($orders !== null) {
            $this->setOrders($orders);
        }
        if ($group !== null) {
            $this->_select->group($group);
        }
        $sql = $this->_select->assemble();
        $this->setSql($sql);
        return $this->_db->fetchOne($sql, $this->_bind);
    }

    public function find($columns = '*', $id) {
        return $this->findByPrimary($id, $columns);
    }

    public function findByPrimary($columns = '*', $id) {
        $this->reset();
        $this->_select->from(array(
            $this->_name => $this->_name
        ));
        $this->setColumns($columns);
        if ($this->_joinRelation && ! empty($this->_relationMap)) {
            $this->_processRelationsByJoin();
        }
        $where = $this->_db->quoteInto("$this->_name.$this->_primary  =?", $id);
        $this->_select->where($where);
        $sql = $this->_select->assemble();
        $this->setSql($sql);
        $this->_row = $this->_db->fetchRow($sql, $this->_bind);
        if ($this->_activeRelations && $this->_row) {
            $this->_processRelation();
        }
        return new Jf_RecordSet($this->_row);
    }

    public function totalCount($filters = null) {
        $this->reset();
        $this->getSelect()->from(array(
            $this->_name => $this->_name
        ), "COUNT($this->_name.$this->_primary)");
        if ($this->_joinRelation) {
            foreach ($this->_relationMap as $table) {
                if (empty($table['joinTable'])) {
                    continue;
                }
                $tableName = $table['tableName'];
                $joinType = empty($table['is_required']) ? 'joinLeft' : 'joinInner';
                $this->_select->{$joinType}(array(
                    $tableName => $this->getDbTable($tableName)
                ), "$this->_name.$this->_primary = $tableName.{$table['fkey']}", null);
            }
        }
        if ($filters !== null) {
            $this->setFilters($filters);
        }
        $sql = $this->_select->assemble();
        $this->setSql($sql);
        $result = $this->_db->fetchOne($sql, $this->_bind);
        return $result;
    }

    public function addIndexCol($column) {
        array_push($this->_indexCols, $column);
        return $this;
    }

    public function setIndexCols(array $columns) {
        $this->_indexCols = $columns;
        return $this;
    }

    protected function setIndexByRows() {
        foreach ($this->_indexCols as $column) {
            $this->_index[$column] = array_column($this->_rows, $column);
        }
        return $this;
    }

    public function getManyToMany() {
        // $mkey 为外键
        foreach ($this->_activeRelations as $alis) {
            $relation = $this->_relationMap[$alis];
            $mkey = $relation['mkey'];
            if (empty($this->_index[$mkey])) {
                continue;
            }
            $fkey = $relation['fkey'];
            $o2m = isset($relation['o2m']) ? $relation['o2m'] : false;
            $o2o = isset($relation['o2o']) ? $relation['o2o'] : false;
            $o2oMerge = isset($relation['o2o_merge']) ? $relation['o2o_merge'] : true;
            $sortOrder = isset($relation['sort_order']) ? $relation['sort_order'] : null;
            $count = isset($relation['count']) ? $relation['count'] : null;
            $group = isset($relation['group']) ? $relation['group'] : null;
            $enableCache = isset($relation['enableCache']) ? $relation['enableCache'] : false;
            $relationModel = Jf::getModel($relation['modelName']);
            if (true === $enableCache) {
                $relationModel->enableCache()->addCacheDependent(get_class($this));
            }
            $columns = (array) $relation['columns'];
            array_unshift($columns, $fkey);
            if (true === $o2o) {
                $manyResult = $relationModel->fetchAll($columns, array(
                    $fkey => $this->_index[$mkey]
                ), $sortOrder, $count, null, $group);
            }
            foreach ($this->_rows as $key => $row) {
                if (true === $o2o && empty($manyResult)) {
                    unset($this->_rows[$key][$alis]);
                    continue;
                }
                if (empty($row[$mkey])) {
                    unset($this->_rows[$key][$alis]);
                    continue;
                }
                $mkeyValue = $row[$mkey];
                if (true === $o2m) {
                    $where = isset($relation['where']) ? $relation['where'] : array();
                    $where[$fkey] = $mkeyValue;
                    $o2mResult = $relationModel->fetchAll($columns, $where, $sortOrder, $count, null, $group);
                    $this->_rows[$key][$alis] = $o2mResult;
                } elseif (true === $o2o) {
                    foreach ($manyResult as $mRow) {
                        if (empty($mRow)) {
                            continue;
                        } elseif ($mkeyValue !== $mRow[$fkey]) {
                            continue;
                        }
                        if (! $this->_relationHasChildren($columns) && ! in_array($fkey, (array) $relation['columns'])) {
                            unset($mRow[$fkey]);
                            if ($fkey !== $this->_primary) {
                                unset($this->_row[$mkey]);
                            }
                        }
                        if ($o2oMerge) {
                            $this->_rows[$key] = array_merge($row, $mRow);
                        } else {
                            $row[$alis] = $mRow;
                            $this->_rows[$key] = $row;
                        }
                    }
                }
            }
        }
        return $this;
    }

    protected function _relationHasChildren($cols) {
        foreach ($cols as $key => $value) {
            if (is_string($key) && is_array($value)) {
                return true;
            }
        }
        return false;
    }

    protected function setIndex() {
        foreach ($this->_indexCols as $column) {
            $this->_select->reset(Zend_Db_Select::COLUMNS)->columns($column);
            $sql = $this->_select->assemble();
            $this->_index[$column] = $this->_db->fetchCol($sql, $this->_bind);
        }
    }

    public function getIndex($column) {
        if (isset($this->_index[$column])) {
            return $this->_index[$column];
        }
    }

    protected function setOrders($orders) {
        if (! $orders) {
            return $this;
        }
        foreach ((array) $orders as $key => $value) {
            if (! in_array(strtoupper($value), array(
                Zend_Db_Select::SQL_ASC,
                Zend_Db_Select::SQL_DESC
            ))) {
                continue;
            }
            if (array_key_exists($key, $this->_orders)) {
                $orderMethod = $this->_orders[$key];
                $this->$orderMethod($value);
            } elseif (in_array($key, $this->_getCols())) {
                $this->getSelect()->order($key . ' ' . $value);
            } else {
                $this->getSelect()->order($key . ' ' . $value);
            }
        }
        return $this;
    }

    protected function setColumns($columns) {
        $this->_select->reset(Zend_Db_Select::COLUMNS);
        $this->_activeRelations = array();
        if (is_string($columns)) {
            $this->getSelect()->columns($columns);
        } elseif (is_array($columns)) {
            $simpleCols = array();
            foreach ($columns as $key => $column) {
                if (is_string($key) && $this->hasProperty('_relationMap')) {
                    if (! empty($this->_relationMap[$key])) {
                        $mKey = $this->_relationMap[$key]['mkey'];
                        $this->_relationMap[$key]['columns'] = $column;
                        array_push($this->_activeRelations, $key);
                        if (empty($this->_relationMap[$key]['joinTable'])) {
                            $this->addIndexCol($mKey);
                        }
                        array_push($simpleCols, $mKey);
                    } elseif (is_string($column)) {
                        $this->getSelect()->columns(array(
                            $key => $column
                        ));
                    }
                } else {
                    array_push($simpleCols, $column);
                }
            }
            $intersect_cols = array_intersect($simpleCols, $this->_getCols());
            $this->getSelect()->columns($intersect_cols, $this->_name);
        }
        return $this;
    }

    protected function hasProperty($property) {
        if (property_exists($this, $property)) {
            return true;
        } else {
            return false;
        }
    }

    public function setFilters($filters) {
        if (! $filters) {
            return $this;
        }
        foreach ($filters as $key => $value) {
            if (is_numeric($key)) {
                $key = $value;
                $value = '';
            }
            if (in_array($key, $this->_getCols())) {
                if (is_array($value)) {
                    $this->addIndexFilter($value, $key);
                } else {
                    $this->addBind($key, $value);
                    $this->getSelect()->where("$key = :$key");
                }
            } else {
                $filterMethod = 'add' . self::_normalizePropertyName($key) . 'Filter';
                if (method_exists($this, $filterMethod)) {
                    $this->$filterMethod($value);
                } else {
                    $this->getSelect()->where($key, $value);
                }
            }
        }
        return $this;
    }

    protected function addBind($key, $value) {
        $this->_bind[$key] = $value;
        return $this;
    }

    protected function resetBind() {
        $this->_bind = array();
        return $this;
    }

    protected static function _normalizePropertyName($property) {
        return str_replace(' ', '', ucwords(str_replace('_', ' ', $property)));
    }

    public function addIndexFilter(array $index, $column = null) {
        $column = isset($column) ? $column : $this->_primary;
        $indexString = trim(implode(',', $index));
        $this->getSelect()->where("$column in ($indexString)");
    }

    public function getDbTableByRelationMap($alias) {
        $tableName = str_replace('/', '_', $this->_relationMap[$alias]['modelName']);
        return $this->getDbTable($tableName);
    }

    public function setRelations(array $relations) {
        foreach ($relations as $alias => $params) {
            $this->setRelation($alias, $params);
        }
        return $this;
    }

    public function setRelation($alias, $params) {
        if ($this->hasRelation($alias)) {
            $relation = array_merge($this->getRelation($alias), $params);
        } else {
            $relation = $params;
        }
        $this->_relationMap[$alias] = $relation;
        array_push($this->_activeRelations, $alias);
        return $this;
    }

    public function getRelation($alias) {
        return $this->_relationMap[$alias];
    }

    public function hasRelation($alias) {
        return isset($this->_relationMap[$alias]) ? true : false;
    }

    public function setJoinRelation($flag) {
        $this->_joinRelation = $flag;
        return $this;
    }

    protected function _autoCount() {
        $activeAutoCount = array_intersect_key($this->_autoCount, $this->_saveData);
        if (empty($activeAutoCount)) {
            return $this;
        }
        foreach ($activeAutoCount as $mkey => $counts) {
            foreach ($counts as $alias => $countName) {
                $relation = $this->getRelation($alias);
                $modelName = $relation['modelName'];
                $fkey = $relation['fkey'];
                $relationModel = Jf::getModel($modelName);
                $countValue = $relationModel->fetchOne($countName, array(
                    $fkey => $this->_saveData[$mkey]
                ));
                $relationModel->insertOnDuplicate(array(
                    $countName => $countValue + 1,
                    $fkey => $this->_saveData[$mkey]
                ), $countName);
            }
        }
        return $this;
    }

    public function getReport(array $where, $columns = '*', $group = null, $orders = null) {
        $this->reset();
        $this->_select->from(array(
            $this->_name => $this->_name
        ));
        $this->setColumns($columns);
        if (! empty($this->_relationMap)) {
            $this->_processRelationsByJoin();
        }
        $this->setFilters($where);
        if ($orders !== null) {
            $this->setOrders($orders);
        }
        if ($group !== null) {
            $this->_select->group($group);
        }
        $sql = $this->_select->assemble();
        $this->_rows = $this->_db->fetchAll($sql, $this->_bind);
        return $this->_rows;
    }

    protected function addCreatedAtBetweenFilter($param) {
        $this->getSelect()->where($this->_prepareBetweenSql('created_at', $param['from'], $param['to']));
        return $this;
    }

    protected function _prepareBetweenSql($field, $from, $to) {
        return sprintf('(%s BETWEEN %s AND %s)', $field, $this->_db->quote($from), $this->_db->quote($to));
    }
}
